package com.citywithincity.pay.wx;

import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;

import javax.servlet.http.HttpServletRequest;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.HttpEntity;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.xml.sax.SAXException;

import com.citywithincity.pay.IPay;
import com.citywithincity.pay.OrderInfo;
import com.citywithincity.pay.PayException;
import com.citywithincity.pay.PayFactory;
import com.citywithincity.pay.PayStatus;
import com.citywithincity.pay.SysOrder;
import com.citywithincity.utils.HttpUtil;
import com.citywithincity.utils.IoUtil;
import com.citywithincity.utils.MD5Util;
import com.citywithincity.utils.MapUtil;
import com.citywithincity.utils.MemoryUtil;
import com.damai.xml.XMLParseException;
import com.damai.xml.XMLUtil;

public class WXPayModel implements IPay {
	
	private static Log log = LogFactory.getLog(WXPayModel.class);
	
	private static final Set<String> runtimeExceptions = new HashSet<String>();
	//用户错误，需要让用户知道
	private static final Map<String, String> userExceptions = new HashMap<String,String>();
	static{
		//如果错误描述包含这些，那么直接报错，只要程序正常，不可能报这个错误
		runtimeExceptions.add("NOAUTH");//商户无此接口权限
		runtimeExceptions.add("APPID_NOT_EXIST");
		runtimeExceptions.add("MCHID_NOT_EXIST");
		runtimeExceptions.add("APPID_MCHID_NOT_MATCH");
		runtimeExceptions.add("LACK_PARAMS");
		runtimeExceptions.add("OUT_TRADE_NO_USED");
		runtimeExceptions.add("SIGNERROR");
		runtimeExceptions.add("XML_FORMAT_ERROR");
		runtimeExceptions.add("REQUIRE_POST_METHOD");
		runtimeExceptions.add("POST_DATA_EMPTY");
		runtimeExceptions.add("NOT_UTF8");
		
		userExceptions.put("TRANSACTION_ID_INVALID", "非法微信订单号");
		userExceptions.put("NOTENOUGH", "您的余额不足");
		userExceptions.put("ORDERPAID", "商户订单已支付，请勿重复支付");
		userExceptions.put("ORDERCLOSED", "当前订单已关闭，无法支付");
		userExceptions.put("NOTENOUGH", "您的余额不足");
		userExceptions.put("ORDERNOTEXIST", "订单不存在");
	}
	
	public static final String QUERY_URL = "https://api.mch.weixin.qq.com/pay/orderquery";
	public static final String REFUND_URL = "https://api.mch.weixin.qq.com/secapi/pay/refund";
	public static final String PRE_PAY_URL = "https://api.mch.weixin.qq.com/pay/unifiedorder";
	public static final String BILL_URL = "https://api.mch.weixin.qq.com/pay/downloadbill";
	
	private static final Object out_trade_no = "out_trade_no";

	private static final String time_end = "time_end";
	
	private String appID;
	private String wxKey;
	private String mchID;
	private CloseableHttpClient httpClient;
	private String channel;
//	private AesCrypt crypt = new AesCrypt();
//CloseableHttpClient httpclient  = HttpClients.custom().setSSLSocketFactory(sslsf).build();

	public WXPayModel(CloseableHttpClient httpClient,
			String appID, 
			String wxKey,
			String mchID,
			String channel) {
		this.appID = appID;
		this.wxKey = wxKey;
		this.mchID = mchID;
		this.channel = channel;
		this.httpClient = httpClient;
	}
	
	
	public InputStream downloadBill(String date) throws IOException, PayException{
		Map<String, Object> data = getInitData();
		data.put("bill_date", date);
		data.put("bill_type", "ALL");
		signData(data);
		InputStream inputStream = tryGetContent(BILL_URL, MemoryUtil.mapToXml(data));
		
  	   	return inputStream;
	}

	/**
	 * 退款接口
	 * 
	 * @param outID
	 * @param fee
	 * @throws Exception 
	 * @throws IOException 
	 */
	@Override
	public boolean refund(SysOrder order,String orderID,String refundID, int fee,boolean isOutID) throws PayException, IOException{
		Map<String, Object> values = getInitData();
		values.put("op_user_id", mchID);
		values.put("out_refund_no", refundID);
		if(isOutID){
			values.put("out_trade_no", orderID);
		}else{
			values.put("transaction_id", orderID);
		}
		
		String refundFee = String.valueOf(fee);
		values.put("refund_fee", refundFee);
		values.put("total_fee", refundFee);
		signData(values);
		IOException exception = null;
		for(int i=0; i<3; ++i){
			try{
				tryPostWithCert(MemoryUtil.mapToXml(values));
				return true;
			}catch(IOException e){
				exception = e;
			}
		}
		//超过3个还不行
		log.error("退款失败",exception);
		throw exception;
	}
	
	
	private static void check(Map<String, String> result) throws IOException, PayException{
		log.info(MapUtil.toString(result));
		if (!SUCCESS.equals(result.get(return_code))) {
			//程序如果正常运行是不会出这个错误的
			throw new RuntimeException(result.get(return_msg));
		}
		if (!SUCCESS.equals(result.get(result_code))) {
			String errCode = result.get(err_code);
			if (SYSTEMERROR.equals(errCode)) {
				//这里需要重试
				throw new IOException("退款超时，请稍后重试");
			}
			//这里是返回的用户信息，需要让用户知道
			if(runtimeExceptions.contains(errCode)){
				throw new RuntimeException(errCode);
			}
			
			if(userExceptions.containsKey(errCode)){
				throw new PayException(userExceptions.get(errCode),errCode);
			}else{
				throw new RuntimeException("微信提示:"+ result.get("err_code_des") );
			}
		}
	}

	/**
	 * 证书啼叫
	 * 
	 * @throws IOException
	 * @throws PayException 
	 * @throws ClientProtocolException
	 * @throws SAXException
	 * @throws ParserConfigurationException
	 * @throws IllegalStateException
	 */
	public Map<String, String> tryPostWithCert(String data) throws IOException, PayException{
		CloseableHttpResponse response = null;
		
		HttpPost post = new HttpPost(REFUND_URL);
		post.setEntity(new StringEntity(data, "UTF-8"));
		response = httpClient.execute(post);
		HttpEntity entity = response.getEntity();
		log.info("微信退款:"+data);
		try {
			if (entity != null) {
				Map<String, String> result = XMLUtil.parseXML(entity.getContent(),false);
				log.info("微信退款返回:"+MemoryUtil.mapToString(result));
				check(result);
				return result;
			} else {
				throw new IOException("Server return null");
			}
		} catch (IllegalStateException e) {
			throw new IOException(e);
		} catch (XMLParseException e) {
			throw new IOException(e);
		} finally {
			EntityUtils.consume(entity);
			if (response != null) {
				try {response.close();} catch (Exception e) {}
			}
		}
	}
	
	private final Map<String, Object> getQueryData(String orderID,boolean isOutID){
		Map<String, Object> map = getInitData();
		if(isOutID){
			map.put("out_trade_no", orderID);
		}else{
			map.put("transaction_id", orderID);
		}
		signData(map);
		return map;
	}

	@Override
	public OrderInfo queryOrder(SysOrder order,String orderID,boolean isOutID) throws PayException, IOException {
		Map<String, String> result = normalPost(QUERY_URL, getQueryData(orderID, isOutID));
		return getOrderInfo(result);
	}
	
	public Map<String, String> queryRawData(String orderID,boolean isOutID) throws PayException, IOException {
		Map<String, String> result = normalPost(QUERY_URL, getQueryData(orderID, isOutID));
		return result;
	}
	
	public static OrderInfo getOrderInfo(Map<String, String> result){
		PayStatus payStatus = getStatus(result);
		OrderInfo orderInfo = new OrderInfo();
		orderInfo.setPayStatus(payStatus);
		orderInfo.setOrderId(result.get(out_trade_no));
		orderInfo.setPlatId(result.get(transaction_id));
	//	orderInfo.setPayTime(result.get(time_end));
		if(result.containsKey(cash_fee)){
			orderInfo.setFee(Integer.parseInt(result.get(cash_fee)));
		}
		return orderInfo;
	}

	/**
	 * 查询状态
	 * 
	 * @param outID
	 * @return
	 * @throws PayException 
	 * @throws IOException 
	 */
	public PayStatus queryStatus(String orderID,SysOrder order,boolean isOutID) throws PayException, IOException {
		
		Map<String, String> result = normalPost(QUERY_URL, getQueryData(orderID, isOutID));
		return getStatus(result);
	}

	
	private static InputStream tryGetContent(String url, String data)
			throws IOException {
		HttpURLConnection connection = null;
		InputStream inputStream = null;
		log.info(data);
		connection = HttpUtil.connect(url, data);
		inputStream = connection.getInputStream();
		
		return inputStream;
	}
	
	
	/**
	 * 提交数据
	 * 
	 * @param url
	 * @param data
	 * @return
	 * @throws IOException
	 * @throws PayException 
	 */
	private static Map<String, String> tryPost(String url, String data)
			throws IOException, PayException {
		HttpURLConnection connection = null;
		InputStream inputStream = null;
		try {
			log.info(data);
			connection = HttpUtil.connect(url, data);
			inputStream = connection.getInputStream();
			Map<String, String> result = XMLUtil.parseXML(inputStream,false);
			check(result);
			return result;
		} catch (XMLParseException e) {
			throw new IOException(e);
		}  finally {
			IoUtil.close(inputStream);
			if (connection != null) {
				connection.disconnect();
			}
		}

	}

	private static Map<String, String> normalPost(String url,Map<String, Object> param) throws IOException,PayException {
		String data = MemoryUtil.mapToXml(param);
		return tryPost(url, data);
	}

	public static final String return_code = "return_code";
	public static final String return_msg = "return_msg";
	public static final String result_code = "result_code";
	// 状态
	public static final String trade_state = "trade_state";

	public static final String NOTPAY = "NOTPAY";
	public static final String SUCCESS = "SUCCESS";
	public static final String REFUND = "REFUND";
	public static final String CLOSED = "CLOSED";
	public static final String REVOKED = "REVOKED";
	public static final String USERPAYING = "USERPAYING";
	public static final String PAYERROR = "PAYERROR";

	public static final String err_code = "err_code";
	public static final String SYSTEMERROR = "SYSTEMERROR";
	public static final String ORDERNOTEXIST = "ORDERNOTEXIST";

	/**
	 * 
	 * @param map
	 * @return
	 */
	private void signData(Map<String, Object> map) {
		String sign = getSign(map);
		map.put("sign", sign);
	}
	
	private boolean verifyData(Map<String, ?> map){
		String orgSign = (String) map.get("sign");
		map.remove("sign");
		String sign = getSign(map);
		return sign.equals(orgSign);
	}
	
	private String getSign(Map<String, ?> map){
		StringBuilder sb = new StringBuilder();
		for (Entry<String, ?> entry : map.entrySet()) {
			sb.append(entry.getKey()).append('=').append(entry.getValue())
					.append('&');
		}
		sb.append("key=").append(wxKey);
		return MD5Util.md5Appkey(sb.toString()).toUpperCase();
	}


	private Map<String, Object> getInitData() {
		Map<String, Object> map = new TreeMap<String, Object>();
		map.put("appid", appID);
		map.put("mch_id", mchID);
		map.put("nonce_str", makeRandom());
		return map;
	}

	private String makeRandom() {
		StringBuilder sb = new StringBuilder();
		sb.append(System.currentTimeMillis());
		sb.append(Math.random());
		return MD5Util.md5Appkey(sb.toString());
	}

	public static final String transaction_id = "transaction_id";
	public static final String cash_fee = "cash_fee";
	
	public static final PayStatus getStatus(Map<String, String> result) {
		String status = result.get(trade_state);
		if (status.equals(NOTPAY)) {
			return PayStatus.PayStatus_NoPay;
		}
		if (status.equals(SUCCESS)) {
			return PayStatus.PayStatus_Success;
		}
		if (status.equals(REFUND)) {
			return PayStatus.PayStatus_Refund;
		}
		if (status.equals(CLOSED) || status.equals(REVOKED)) {
			return PayStatus.PayStatus_Close;
		}
		if (status.equals(USERPAYING)) {
			return PayStatus.PayStatus_Paying;
		}
		if (status.equals(PAYERROR)) {
			return PayStatus.PayStatus_Error;
		}
		return PayStatus.PayStatus_NotFound;
	}

	private String getNotifyUrl(int businessId,int type,String channel){
		StringBuilder sb = new StringBuilder(50);
		sb.append(PayFactory.getNotifyUrl())
				.append(businessId).append('-').append(type);
		if(channel!=null){
			sb.append('-').append(channel);
		}
		return sb.toString();
	}
	
	@Override
	public Object prePay(SysOrder order,
			String outID, 
			String title, 
			int fee, 
			int businessId,
			int type,
			String channel,
			HttpServletRequest request) throws PayException, IOException {
		Map<String, String> map = submitOrder(outID, title, fee, getNotifyUrl(businessId,type,channel), request.getRemoteAddr());
		Map<String, Object> values = new TreeMap<String,Object>();
		values.put("appid", appID);
		values.put("noncestr", map.get("nonce_str"));
		values.put("package", "Sign=WXPay");
		
		values.put("partnerid", mchID);
		values.put("prepayid", map.get("prepay_id"));
		values.put("timestamp", String.valueOf(System.currentTimeMillis()/1000));
		signData(values);
		//微信完成，这里开始本身业务逻辑
		values.put("out_id", outID);
		values.put("fee", fee);
		values.put("title", title);
		
		
		return values;
	}
	
	
	/**
	 * 统一下单接口调用
	 * @return
	 * @throws IOException 
	 * @throws PayException 
	 */
	private Map<String, String> submitOrder(String outID,String title,int fee,String notifyUrl,String ip) throws IOException, PayException{
		Map<String, Object> values = getInitData();
		values.put("body", title);
		values.put("notify_url",notifyUrl);
		values.put("out_trade_no", outID);
		values.put("spbill_create_ip", ip);
		values.put("total_fee", fee);
		values.put("trade_type", "APP");
		
		signData(values);
		Map<String, String> map = tryPost(PRE_PAY_URL, MemoryUtil.mapToXml(values));
		map.put("nonce_str", (String) values.get("nonce_str"));
		return map;
	}

	@Override
	public boolean clientVerify(String content, String deviceID) {
	//	String result = crypt.decript(content, deviceID);
		
		return false;
	}

	@Override
	public OrderInfo parseOrder(HttpServletRequest request) throws IOException {
		InputStream inputStream = null;
		try {
			inputStream = request.getInputStream();
			Map<String, String> map = XMLUtil.parseXML(inputStream,true);
			if(!verifyData(map)){
				throw new IOException("签名错误");
			}
			OrderInfo orderInfo = new OrderInfo();
			orderInfo.setOrderId(map.get(out_trade_no));
			orderInfo.setPlatId(map.get(transaction_id));
			orderInfo.setPayStatus(PayStatus.PayStatus_Success);
			return orderInfo;
		} catch (XMLParseException e) {
			throw new IOException(e);
		}finally{
			IoUtil.close(inputStream);
		}
	}

	@Override
	public PayStatus queryClientNotifyOrderStatus(String id, SysOrder order, Map<String, Object> json) throws PayException, IOException {
		
		return queryStatus(id, order,true);
	}

}
